﻿// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

using Nova.CodeDOM;
using Nova.Utilities;
using Attribute = Nova.CodeDOM.Attribute;

namespace Nova.Rendering
{
    /// <summary>
    /// Used to format code objects as text and write them into a string or a file.
    /// </summary>
    /// <remarks>
    /// The main purpose of this class is to keep track of the indentation level, and to emit the spaces
    /// or tabs to create the indentation as the first text is written to each line.  It also supports
    /// "pending comments" that are written in block style if not the last thing on the line.  And, it
    /// keeps track of the current line number, and fires events when newlines are created.
    /// </remarks>
    public class CodeWriter : IDisposable
    {
        #region /* STATIC FIELDS */

        /// <summary>
        /// C# keywords.
        /// </summary>
        public static HashSet<string> Keywords = new HashSet<string>
            {
                "abstract", "as",       "base",       "bool",      "break",
                "byte",     "case",     "catch",      "char",      "checked",
                "class",    "const",    "continue",   "decimal",   "default",
                "delegate", "do",       "double",     "else",      "enum",
                "event",    "explicit", "extern",     "false",     "finally",
                "fixed",    "float",    "for",        "foreach",   "goto",
                "if",       "implicit", "in",         "int",       "interface",
                "internal", "is",       "lock",       "long",      "namespace",
                "new",      "null",     "object",     "operator",  "out",
                "override", "params",   "private",    "protected", "public",
                "readonly", "ref",      "return",     "sbyte",     "sealed",
                "short",    "sizeof",   "stackalloc", "static",    "string",
                "struct",   "switch",   "this",       "throw",     "true",
                "try",      "typeof",   "uint",       "ulong",     "unchecked",
                "unsafe",   "ushort",   "using",      "virtual",   "void",
                "volatile", "while"
                // Many of these keywords could theoretically be contextual, but they're not historically, so we
                // have to treat them as keywords at least in text sources to be compatible with the C# compiler.
            };

        #endregion

        #region /* FIELDS */

        protected TextWriter _textWriter;
        protected int _lineNumber = 1;
        protected int _columnNumber = 1;
        protected bool _isEmptyLine = true;
        protected bool _isGenerated;
        protected List<EOLComment> _pendingEOLComments = new List<EOLComment>();
        protected bool _flushingEOLComments;

        protected Stack<IndentState> _indentStateStack = new Stack<IndentState>(32);
        protected IndentState _lastPoppedIndentState;
        protected Stack<AlignmentState> _alignmentStateStack = new Stack<AlignmentState>(16);

        /// <summary>
        /// True if rendering documentation comment content.
        /// </summary>
        public bool InDocCommentContent;

        /// <summary>
        /// True if unicode characters should be escaped.
        /// </summary>
        public bool EscapeUnicode = true;

        /// <summary>
        /// True if tabs should be used for indentation instead of spaces.
        /// </summary>
        public bool UseTabs;

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a code writer that writes to a string.
        /// </summary>
        public CodeWriter(bool calculateOnly, bool isGenerated)
        {
            if (!calculateOnly)
                _textWriter = new StringWriter();
            _indentStateStack.Push(new IndentState(null, 0, 0, 0));
            _isGenerated = isGenerated;
        }

        /// <summary>
        /// Create a code writer that writes to a string.
        /// </summary>
        public CodeWriter(bool calculateOnly)
            : this(calculateOnly, false)
        { }

        /// <summary>
        /// Create a code writer that writes to a string.
        /// </summary>
        public CodeWriter()
            : this(false, false)
        { }

        /// <summary>
        /// Create a code writer that writes to a text file.
        /// </summary>
        public CodeWriter(string fileName, Encoding encoding, bool hasUTF8BOM, bool useTabs, bool isGenerated)
        {
            // Create the specified output directory if it doesn't exist
            string directory = Path.GetDirectoryName(fileName);
            if (!string.IsNullOrEmpty(directory))
                Directory.CreateDirectory(directory);

            // Specify the encoding for the output, with special logic to omit the UTF8 BOM if it wasn't there originally
            FileStream fileStream = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
            if (encoding == Encoding.UTF8 && !hasUTF8BOM)
                encoding = new UTF8Encoding(false);
            _textWriter = new StreamWriter(fileStream, encoding);
            _indentStateStack.Push(new IndentState(null, 0, 0, 0));
            UseTabs = useTabs;
            _isGenerated = isGenerated;
        }

        /// <summary>
        /// Create a code writer that writes to a text file.
        /// </summary>
        public CodeWriter(string fileName, Encoding encoding, bool hasUTF8BOM, bool useTabs)
            : this(fileName, encoding, hasUTF8BOM, useTabs, false)
        { }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The current line number (1 to N).
        /// </summary>
        public int LineNumber
        {
            get { return _lineNumber; }
        }

        /// <summary>
        /// The current column number (1 to N).
        /// </summary>
        public int ColumnNumber
        {
            get { return _columnNumber; }
        }

        /// <summary>
        /// True if the code being rendered is generated (such as a generated '.g.cs' file).  Code cleanup settings will be ignored.
        /// </summary>
        public bool IsGenerated
        {
            get { return _isGenerated; }
        }

        /// <summary>
        /// Get or set the string used to create new lines (LF or CR/LF).
        /// </summary>
        public string NewLine
        {
            get { return (_textWriter != null ? _textWriter.NewLine : null); }
            set
            {
                if (_textWriter != null)
                    _textWriter.NewLine = value;
            }
        }

        /// <summary>
        /// True if a newline is required before any other text, such as if a compiler directive was just emitted
        /// (used to force a newline before a terminating ';' on an expression with a compiler directive at the end).
        /// </summary>
        public bool NeedsNewLine { get; set; }

        /// <summary>
        /// Get or set the current indent offset (0 to N).
        /// </summary>
        public int IndentOffset
        {
            get { return _indentStateStack.Peek().IndentOffset; }
            set
            {
                _indentStateStack.Peek().IndentOffset = value;
                if (_isEmptyLine)
                    _columnNumber = value + 1;
            }
        }

        /// <summary>
        /// A stack of <see cref="AlignmentState"/>s.
        /// </summary>
        public Stack<AlignmentState> AlignmentStateStack
        {
            get { return _alignmentStateStack; }
            set { _alignmentStateStack = value; }
        }

        /// <summary>
        /// This event is fired after a new line is created.
        /// </summary>
        public event Action<CodeWriter> AfterNewLine;

        #endregion

        #region /* METHODS */

        /// <summary>
        /// Write the specified text.
        /// </summary>
        public void Write(string text)
        {
            if (string.IsNullOrEmpty(text))
                return;

            if (_pendingEOLComments.Count > 0)
                FlushPendingEOLComments(false);

            if (NeedsNewLine)
                WriteLine("");

            if (_isEmptyLine)
            {
                _isEmptyLine = false;

                // If we're writing the first text on the line, indent first
                int indentOffset = _indentStateStack.Peek().IndentOffset;
                if (_textWriter != null)
                {
                    string indentString = (UseTabs ? new string('\t', indentOffset / CodeObject.TabSize) + new string(' ', indentOffset % CodeObject.TabSize)
                        : new string(' ', indentOffset));
                    _textWriter.Write(indentString);
                }
                _columnNumber = indentOffset + 1;

                // If using tabs, also use tabs for any leading spaces in the text (this can occur in block comments
                // or skipped sections of conditional directives).
                if (_textWriter != null && UseTabs && text[0] == ' ')
                {
                    int leadingSpaces = StringUtil.CharCount(text, ' ', 0);
                    int tabs = leadingSpaces / CodeObject.TabSize;
                    _textWriter.Write(new string('\t', tabs));
                    int offset = tabs * CodeObject.TabSize;
                    _columnNumber += offset;
                    text = text.Substring(offset);
                }
            }

            // Scan the output text for any required encoding
            StringBuilder result = null;
            int i, start = 0;
            for (i = 0; i < text.Length; ++i)
            {
                string encoded = null;
                if (InDocCommentContent)
                {
                    // Encode any '&', '<', '>' chars if we're in doc comment content
                    switch (text[i])
                    {
                        case '&': encoded = "&amp;"; break;
                        case '<': encoded = "&lt;"; break;
                        case '>': encoded = "&gt;"; break;
                    }
                }

                // Encode any unicode chars if so requested
                if (text[i] > 0xff && EscapeUnicode)
                {
                    if (char.IsSurrogatePair(text, i))
                    {
                        int u32 = char.ConvertToUtf32(text, i);
                        encoded = string.Format(@"\U{0:x8}", u32);
                    }
                    else
                        encoded = string.Format(@"\u{0:x4}", (int)text[i]);
                }

                if (encoded != null)
                {
                    if (result == null)
                        result = new StringBuilder();
                    result.Append(text, start, i - start);
                    result.Append(encoded);
                    start = i + 1;
                }
            }
            if (result != null)
                text = result.Append(text, start, i - start).ToString();

            // Write the text
            if (_textWriter != null)
                _textWriter.Write(text);
            _columnNumber += text.Length;
        }

        /// <summary>
        /// Write an identifier, prefixing with '@' if it happens to be a keyword.
        /// </summary>
        public void WriteIdentifier(string text, CodeObject.RenderFlags flags)
        {
            // Delimit the identifier if we're not in a doc comment and it's the same as a C# keyword
            if (!flags.HasFlag(CodeObject.RenderFlags.InDocComment) && Keywords.Contains(text))
                Write("@");
            Write(text);
        }

        /// <summary>
        /// Render a name, hiding any 'Attribute' suffix if it's an attribute name.
        /// </summary>
        public void WriteName(string name, CodeObject.RenderFlags flags, bool possibleKeyword)
        {
            // Hide any "Attribute" suffix for attribute constructor names
            if (flags.HasFlag(CodeObject.RenderFlags.Attribute) && name.EndsWith(Attribute.NameSuffix))
            {
                name = name.Substring(0, name.Length - Attribute.NameSuffix.Length);
                possibleKeyword = false;
            }
            if (possibleKeyword)
                WriteIdentifier(name, flags);
            else
                Write(name);
        }

        /// <summary>
        /// Render a name, hiding any 'Attribute' suffix if it's an attribute name.
        /// </summary>
        public void WriteName(string name, CodeObject.RenderFlags flags)
        {
            WriteName(name, flags, false);
        }

        /// <summary>
        /// Write optional text followed by a newline.
        /// </summary>
        public void WriteLine(string text)
        {
            if (text != null)
                Write(text);

            if (_pendingEOLComments.Count > 0)
                FlushPendingEOLComments(true);

            if (_textWriter != null)
                _textWriter.WriteLine();
            ++_lineNumber;
            _columnNumber = 1;
            _isEmptyLine = true;
            NeedsNewLine = false;

            // Set any new indentation for the new line
            IndentOffset = _indentStateStack.Peek().IndentOffsetOnNewLine;

            if (AfterNewLine != null)
                AfterNewLine(this);
        }

        /// <summary>
        /// Write optional text followed by a newline.
        /// </summary>
        public void WriteLine()
        {
            WriteLine(null);
        }

        /// <summary>
        /// Write the specified number of newlines.
        /// </summary>
        public void WriteLines(int count)
        {
            for (int i = 0; i < count; ++i)
                WriteLine();
        }

        /// <summary>
        /// Write a list of CodeObjects.
        /// </summary>
        public void WriteList<T>(IEnumerable<T> enumerable, CodeObject.RenderFlags flags, CodeObject parent, int[] columnWidths) where T : CodeObject
        {
            if (enumerable == null)
                return;

            // Increase the indent level for any newlines that occur within the child list unless specifically told not to
            bool increaseIndent = !flags.HasFlag(CodeObject.RenderFlags.NoIncreaseIndent);
            if (increaseIndent)
                BeginIndentOnNewLine(parent);

            // Render the items in the list
            bool isSingleLine = true;
            bool isFirst = true;
            int column = 0;
            IEnumerator<T> enumerator = enumerable.GetEnumerator();
            bool hasMore = enumerator.MoveNext();
            while (hasMore)
            {
                CodeObject codeObject = enumerator.Current;
                hasMore = enumerator.MoveNext();
                if (codeObject != null)
                {
                    if (codeObject.IsFirstOnLine)
                    {
                        isSingleLine = false;
                        column = 0;
                    }

                    // Render any newlines here, so that the indentation will be correct if the object has any post annotations
                    // (which are rendered separately below, so that any commas can be rendered properly).
                    if (!flags.HasFlag(CodeObject.RenderFlags.IsPrefix) && !flags.HasFlag(CodeObject.RenderFlags.SuppressNewLine) && codeObject.NewLines > 0)
                    {
                        WriteLines(codeObject.NewLines);
                        // Set the parent offset for any post annotations
                        SetParentOffset();
                    }

                    // Render the code object, omitting any EOL comments and post annotations (so they can be rendered later after
                    // any comma), and prefixing a space if it's not the first item.
                    CodeObject.RenderFlags passFlags = flags | CodeObject.RenderFlags.NoEOLComments | CodeObject.RenderFlags.NoPostAnnotations | CodeObject.RenderFlags.SuppressNewLine
                        | (isSingleLine ? (isFirst ? 0 : CodeObject.RenderFlags.PrefixSpace) : (codeObject.IsFirstOnLine ? 0 : CodeObject.RenderFlags.PrefixSpace));
                    codeObject.AsText(this, passFlags);
                    flags &= ~(CodeObject.RenderFlags.SuppressNewLine | CodeObject.RenderFlags.NoPreAnnotations);

                    if (hasMore)
                    {
                        // Render the trailing comma, with any EOL comments before or after it, depending on whether or not it's the last thing on the line
                        CodeObject nextObject = enumerator.Current;
                        bool isLastOnLine = (nextObject != null && nextObject.IsFirstOnLine);
                        if (!isLastOnLine)
                            codeObject.AsTextEOLComments(this, flags);
                        if (!flags.HasFlag(CodeObject.RenderFlags.NoItemSeparators))
                            Write(Expression.ParseTokenSeparator);
                        if (!isLastOnLine || codeObject.HasEOLComments)
                            WritePadding(codeObject, columnWidths, column);
                        if (isLastOnLine)
                            codeObject.AsTextEOLComments(this, flags);
                    }
                    else
                    {
                        if (flags.HasFlag(CodeObject.RenderFlags.HasTerminator) && !flags.HasFlag(CodeObject.RenderFlags.Description) && parent != null)
                            ((Statement)parent).AsTextTerminator(this, flags);
                        bool hasCloseBraceOnSameLine = (parent is Initializer && !((Initializer)parent).IsEndFirstOnLine);
                        if (codeObject.HasEOLComments || hasCloseBraceOnSameLine)
                        {
                            WritePadding(codeObject, columnWidths, column);

                            // If we're aligning columns and it's a multi-line list, add an extra space to line up the EOL comment
                            // or close brace since there's no trailing comma.
                            if (columnWidths != null && !isSingleLine)
                            {
                                if (_textWriter != null)
                                    _textWriter.Write(' ');
                                ++_columnNumber;
                            }
                        }
                        codeObject.AsTextEOLComments(this, flags);
                    }
                    codeObject.AsTextAnnotations(this, AnnotationFlags.IsPostfix, flags);
                }
                else if (hasMore)
                    Write(Expression.ParseTokenSeparator);

                isFirst = false;
                ++column;
            }

            // Reset the indent level
            if (increaseIndent)
                EndIndentation(parent);
        }

        /// <summary>
        /// Write a list of CodeObjects.
        /// </summary>
        public void WriteList<T>(IEnumerable<T> enumerable, CodeObject.RenderFlags flags, CodeObject parent) where T : CodeObject
        {
            WriteList(enumerable, flags, parent, null);
        }

        private void WritePadding(CodeObject codeObject, int[] columnWidths, int column)
        {
            if (columnWidths != null && column < columnWidths.Length)
            {
                int paddingLength = columnWidths[column] - codeObject.AsTextLength(CodeObject.RenderFlags.LengthFlags, AlignmentStateStack);
                if (paddingLength > 0)
                {
                    if (_textWriter != null)
                        _textWriter.Write(new string(' ', paddingLength));
                    _columnNumber += paddingLength;
                }
            }
        }

        /// <summary>
        /// Set the indent offset of the parent object.
        /// </summary>
        public void SetParentOffset()
        {
            _indentStateStack.Peek().ParentOffset = _columnNumber - 1;
        }

        /// <summary>
        /// Begin a section during which any newline should be indented an extra level.
        /// </summary>
        public void BeginIndentOnNewLine(CodeObject codeObject)
        {
            IndentState indentState = _indentStateStack.Peek();
            _indentStateStack.Push(new IndentState(codeObject, indentState.IndentOffset,
                indentState.IndentOffset + CodeObject.TabSize, indentState.ParentOffset));
        }

        /// <summary>
        /// Begin a section during which any newline should be indented relative to the parent object offset.
        /// </summary>
        public void BeginIndentOnNewLineRelativeToParentOffset(CodeObject codeObject, bool additionalIndent)
        {
            IndentState indentState = _indentStateStack.Peek();
            _indentStateStack.Push(new IndentState(codeObject, indentState.IndentOffset,
                indentState.ParentOffset + (additionalIndent ? CodeObject.TabSize : 0), indentState.ParentOffset));
        }

        /// <summary>
        /// Begin a section during which any newline should be indented relative to the current offset.
        /// </summary>
        public void BeginIndentOnNewLineRelativeToCurrentOffset(CodeObject codeObject)
        {
            IndentState indentState = _indentStateStack.Peek();
            int newOffset = _columnNumber - 1;
            _indentStateStack.Push(new IndentState(codeObject, newOffset, newOffset, indentState.ParentOffset));
            IndentOffset = newOffset;  // Change current column now if line is empty
        }

        /// <summary>
        /// Begin a section during which any newline should be indented relative to the last indented offset.
        /// </summary>
        public void BeginIndentOnNewLineRelativeToLastIndent(CodeObject codeObject, CodeObject lastCodeObject)
        {
            // Use the last indented offset if the code object matches, otherwise just do a normal indent
            IndentState indentState = _lastPoppedIndentState;
            if (indentState == null || indentState.IndentObject != lastCodeObject)
                BeginIndentOnNewLine(codeObject);
            else
            {
                indentState.IndentObject = codeObject;
                _indentStateStack.Push(indentState);
            }
            IndentOffset = IndentOffset;  // Change current column now if line is empty
        }

        /// <summary>
        /// Begin a section during which any newline should be outdented by a certain amount, or to a certain offset.
        /// </summary>
        /// <param name="codeObject">The related code object.</param>
        /// <param name="offset">The indentation offset (0 to N), or amount to outdent if negative.</param>
        public void BeginOutdentOnNewLine(CodeObject codeObject, int offset)
        {
            IndentState indentState = _indentStateStack.Peek();
            int newOffset = (offset < 0 ? indentState.IndentOffsetOnNewLine + offset : offset);
            if (newOffset < 0) newOffset = 0;
            _indentStateStack.Push(new IndentState(codeObject, newOffset, newOffset, indentState.ParentOffset));
            IndentOffset = newOffset;  // Change current column now if line is empty
        }

        /// <summary>
        /// End a section during which any newline should be indented an extra level.
        /// </summary>
        public void EndIndentation(CodeObject codeObject)
        {
            _lastPoppedIndentState = _indentStateStack.Pop();
            if (_lastPoppedIndentState.IndentObject != codeObject)
                Log.WriteLine("ERROR popping indent state stack - objects don't match!");
            IndentOffset = IndentOffset;  // Change current column now if line is empty
        }

        /// <summary>
        /// Get the indentation offset of the specified code object.
        /// </summary>
        public int GetIndentOffset(CodeObject codeObject)
        {
            foreach (IndentState indentState in _indentStateStack)
            {
                if (indentState.IndentObject == codeObject)
                    return indentState.IndentOffset;
            }
            return 0;
        }

        /// <summary>
        /// Begin the association of alignment information with a code object.
        /// </summary>
        public void BeginAlignment(CodeObject codeObject, int[] alignmentOffsets)
        {
            _alignmentStateStack.Push(new AlignmentState(codeObject, alignmentOffsets));
        }

        /// <summary>
        /// Get any column widths associated with the specified CodeObject.
        /// </summary>
        public int[] GetColumnWidths(CodeObject codeObject)
        {
            foreach (AlignmentState alignmentState in _alignmentStateStack)
            {
                if (alignmentState.Object == codeObject)
                    return alignmentState.Offsets;
            }
            return null;
        }

        /// <summary>
        /// Get the column width associated with the specified CodeObject.
        /// </summary>
        public int GetColumnWidth(CodeObject codeObject, int column)
        {
            foreach (AlignmentState alignmentState in _alignmentStateStack)
            {
                if (alignmentState.Object == codeObject && column < alignmentState.Offsets.Length)
                    return alignmentState.Offsets[column];
            }
            return 0;
        }

        /// <summary>
        /// End the association of alignment information with a code object.
        /// </summary>
        public void EndAlignment(CodeObject codeObject)
        {
            AlignmentState alignmentState = _alignmentStateStack.Pop();
            if (alignmentState.Object != codeObject)
                Log.WriteLine("ERROR popping alignment state stack - objects don't match!");
        }

        /// <summary>
        /// Write a pending EOL comment, to be flushed later once it's known if anything follows it on the same line.
        /// </summary>
        public void WritePendingEOLComment(Comment comment, CodeObject.RenderFlags flags)
        {
            // If the EOL comment is on a new line (postfix comment), flush any existing pending EOL comments first
            if (comment.IsFirstOnLine && _pendingEOLComments.Count > 0)
                FlushPendingEOLComments(true);

            // Save the comment until the next write, so we can determine if it's the last thing on the line.
            // Also save the indentation of the parent, in case it's actually a postfix comment on a new line (for
            // an Expression).  Use the max of any ParentOffset or the current IndentOffset.
            // Also save the rendering flags, to preserve flags such as UpdateLineCol for later rendering.
            int parentOffset = _indentStateStack.Peek().ParentOffset;
            _pendingEOLComments.Add(new EOLComment(comment, Math.Max(parentOffset, IndentOffset), flags & CodeObject.RenderFlags.PassMask));
        }

        protected void FlushPendingEOLComments(bool isEndOfLine)
        {
            if (!_flushingEOLComments)  // Prevent re-entry
            {
                // Preserve 'NeedsNewLine' state and clear it during this operation
                bool needsNewLine = NeedsNewLine;
                NeedsNewLine = false;
                _flushingEOLComments = true;
                foreach (EOLComment eolComment in _pendingEOLComments)
                {
                    // If the comment is the first thing on the line, we have to restore the original indentation
                    // offset temporarily before rendering it.
                    Comment comment = eolComment.Comment;
                    if (comment.IsFirstOnLine)
                        BeginOutdentOnNewLine(comment, eolComment.Indentation);
                    comment.AsText(this, eolComment.RenderFlags | (isEndOfLine ? 0 : CodeObject.RenderFlags.CommentsInline));
                    if (comment.IsFirstOnLine)
                    {
                        EndIndentation(comment);
                        needsNewLine = false;  // Force off if we emitted a newline
                    }
                }
                _pendingEOLComments.Clear();
                _flushingEOLComments = false;
                NeedsNewLine = needsNewLine;
            }
        }

        /// <summary>
        /// Flush any pending data.
        /// </summary>
        public void Flush()
        {
            FlushPendingEOLComments(true);
        }

        /// <summary>
        /// Convert all written data to a string.
        /// </summary>
        public override string ToString()
        {
            return (_textWriter != null ? _textWriter.ToString() : "");
        }

        /// <summary>
        /// Dispose the object.
        /// </summary>
        public void Dispose()
        {
            if (_textWriter != null)
                _textWriter.Dispose();
        }

        #endregion

        #region /* INDENT STATE */

        /// <summary>
        /// State information for the current indentation offset (related to a CodeObject).
        /// </summary>
        protected class IndentState
        {
            public CodeObject IndentObject;
            public int IndentOffset;
            public int IndentOffsetOnNewLine;
            public int ParentOffset;

            public IndentState(CodeObject indentObject, int indentOffset, int indentOffsetOnNewLine, int parentOffset)
            {
                IndentObject = indentObject;
                IndentOffset = (indentOffset >= 0 ? indentOffset : 0);
                IndentOffsetOnNewLine = (indentOffsetOnNewLine >= 0 ? indentOffsetOnNewLine : 0);
                ParentOffset = parentOffset;
            }
        }

        #endregion

        #region /* ALIGNMENT STATE */

        /// <summary>
        /// Alignment state information related to a <see cref="CodeObject"/>.
        /// </summary>
        public class AlignmentState
        {
            /// <summary>
            /// The <see cref="CodeObject"/>.
            /// </summary>
            public CodeObject Object;

            /// <summary>
            /// Alignment offsets.
            /// </summary>
            public int[] Offsets;

            /// <summary>
            /// Create an <see cref="AlignmentState"/>.
            /// </summary>
            public AlignmentState(CodeObject obj, int[] offsets)
            {
                Object = obj;
                Offsets = offsets;
            }
        }

        #endregion

        #region /* EOL COMMENT */

        /// <summary>
        /// EOL comment and flags for delayed rendering.
        /// </summary>
        public class EOLComment
        {
            /// <summary>
            /// The EOL <see cref="Comment"/>.
            /// </summary>
            public Comment Comment;

            /// <summary>
            /// The indentation of the comment.
            /// </summary>
            public int Indentation;

            /// <summary>
            /// The rendering flags.
            /// </summary>
            public CodeObject.RenderFlags RenderFlags;

            /// <summary>
            /// Create an <see cref="EOLComment"/>.
            /// </summary>
            public EOLComment(Comment comment, int indentation, CodeObject.RenderFlags flags)
            {
                Comment = comment;
                Indentation = indentation;
                RenderFlags = flags;
            }
        }

        #endregion
    }
}
